package com.haredot.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.haredot.dto.UserDTO;
import com.haredot.properties.JwtProperties;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class JwtUtils {


    public static Map<String, Object> generatorAccessToken(UserDetails userDetails, JwtProperties jwtProperties, boolean duration) throws Exception {
        return generatorAccessToken(null, userDetails, jwtProperties, duration, true) ;
    }
    /**
     * 访问令牌
     * @param userDetails
     * @param jwtProperties
     * @return
     */
    public static Map<String, Object> generatorAccessToken(StringRedisTemplate stringRedisTemplate, UserDetails userDetails, JwtProperties jwtProperties, boolean duration, boolean forceString) throws Exception {
        Instant now = Instant.now();

        Instant expired = now.plus(jwtProperties.getExpired());

        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

        List<String> authoritiesList = new ArrayList<>() ;
        if (!CollectionUtils.isEmpty(authorities)) {
            // 获取 简易版权限， 存储到令牌中，减少令牌的长高度
            authoritiesList = authorities.stream().map(auth -> auth.getAuthority()).collect(Collectors.toList());
        }

        // 根据反射 获取 userDetails 中 对应 ID
        Class<? extends UserDetails> aClass = userDetails.getClass();
        Field idField = aClass.getDeclaredField("id");
        Object uid = null ;
        if (idField != null) {
            idField.setAccessible(true);
            uid = idField.get(userDetails);
        }
        // 生成 JWT 令牌, 多加一个 当前用户ID
        String token = JWT.create().withSubject(userDetails.getUsername())
                .withExpiresAt(expired)
                .withIssuer(jwtProperties.getIss())
                .withIssuedAt(now)
                .withClaim("authorities", authoritiesList)
                .withClaim("uid", Long.parseLong(String.valueOf(uid)))
                .withJWTId(Base64.getEncoder().encodeToString((userDetails.getUsername() + ":" + now.getEpochSecond()).getBytes(StandardCharsets.UTF_8)))
                .sign(Algorithm.HMAC256(jwtProperties.getKey()));
        // 将令牌进行压缩处理

        UserDTO userDTO = new UserDTO()  ;
        if (!forceString && stringRedisTemplate != null) {
             // 将 userDetails中的内容拷贝到 UserDTO中
            BeanUtils.copyProperties(userDetails, userDTO);

            Boolean aBoolean = stringRedisTemplate.hasKey("svip:" + userDTO.getId());
            userDTO.setSvip(aBoolean);
            if (aBoolean) {
                // 获取剩余过期 时间
                Long expireDay = stringRedisTemplate.getExpire("svip:" + userDTO.getId(), TimeUnit.DAYS);
                userDTO.setLifeDay(expireDay);
            }
        }

        return Map.of("accessToken", JWTGzipCompression.compressJWTToken(token), "exp",
                duration ? jwtProperties.getExpired().toSeconds() :expired.getEpochSecond(),
                "user", forceString ? userDetails.getUsername() : userDTO ,
                "authorities", authorities == null ? new ArrayList<>(): authorities);

    }

    /**
     * 刷新令牌 username + exp + MD5( username + exp + password + key )
     *
     * @param username
     * @param password
     * @param jwtProperties
     * @return
     */
    public static String generatorRefreshToken(String username, String password, JwtProperties jwtProperties) {

        Instant instant = Instant.now().plus(jwtProperties.getRefreshExpired());
        // 获取 距离 1970 年的秒数
        String epochSecond = String.valueOf(instant.getEpochSecond());

        String formatted = MessageFormat.format("{0}:{1}:{2}:{3}", username, epochSecond, password, jwtProperties.getKey());

        String text = MD5Utils.md5(formatted);

        String format = MessageFormat.format("{0}:{1}:{2}", username, epochSecond, text);

        String jwtId = Base64.getEncoder().encodeToString(format.getBytes(StandardCharsets.UTF_8));
        // 生成 JWT 令牌
        return JWT.create().withSubject(username)
                .withAudience(username)
                .withExpiresAt(instant)
                .withIssuer(jwtProperties.getIss())
                .withIssuedAt(Instant.now())
                .withJWTId(jwtId)
                .sign(Algorithm.HMAC256(jwtProperties.getKey()));
    }


    public static DecodedJWT validToken(JwtProperties jwtProperties, String token) {
        try {
            DecodedJWT verify = JWT.require(Algorithm.HMAC256(jwtProperties.getKey()))
                    .build()
                    .verify(token);
            return verify;
        } catch (SignatureVerificationException e) {
            throw new InvalidCookieException("token验证失败");
        } catch (TokenExpiredException e) {
            throw new InvalidCookieException("token令牌过期");
        }

    }

    /**
     * 生成令牌, 并响应结果
     *
     * @param userDetails
     * @param jwtProperties
     * @param response
     * @throws IOException
     */
    public static void generatorToken(UserDetails userDetails, JwtProperties jwtProperties, HttpServletResponse response) throws Exception {
        generatorToken(userDetails, jwtProperties, response, true, false);
    }

    public static Map<String, Object> generatorToken(StringRedisTemplate stringRedisTemplate , UserDetails userDetails, JwtProperties jwtProperties, boolean refresh, boolean duration, boolean forceString) throws Exception {
        // 生成 JWT 令牌
        try {
            Map<String, Object> accessTokenMap = generatorAccessToken(stringRedisTemplate, userDetails, jwtProperties, duration, forceString);

            String refreshToken = null;
            if (refresh) {
                refreshToken = generatorRefreshToken(userDetails.getUsername(), userDetails.getPassword(), jwtProperties);
            }

            // 将 token 令牌 以 JSON 格式返回
            Map<String, Object> result = Map.of("type", "jwt", "refreshToken", refreshToken);

            Map<String, Object> objectObjectHashMap = new HashMap<>(accessTokenMap);
            objectObjectHashMap.putAll(result);

            return objectObjectHashMap ;
        } catch (Exception e) {
            throw e ;
        }
    }

    public static void generatorToken(UserDetails userDetails, JwtProperties jwtProperties, HttpServletResponse response, boolean refresh, boolean duration) {
        generatorToken(null, userDetails, jwtProperties, response, refresh, duration, true);
    }

    /**
     * @param userDetails
     * @param jwtProperties
     * @param response
     * @param refresh       是否生成 刷新令牌
     * @param duration  true exp 代表过期间隔时间，  false 代表过期时间点
     * @throws IOException
     */
    public static void generatorToken(StringRedisTemplate stringRedisTemplate, UserDetails userDetails, JwtProperties jwtProperties, HttpServletResponse response, boolean refresh, boolean duration, boolean forceString) {
        // 生成 JWT 令牌
        try {
            Map<String, Object> stringObjectMap = generatorToken(stringRedisTemplate, userDetails, jwtProperties, refresh, duration, forceString);
            // 设置 状态为 true
            stringObjectMap.put("status", true) ;

            new MappingJackson2HttpMessageConverter()
                    .write(stringObjectMap, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
        } catch (Exception e) {
            throw new RuntimeException(e) ;
        }
    }


}
